/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * @file sprite2D.cpp
 * @author TooOld2Rock'nRoll
 * @see sprite2D.hpp
 */
/*---- Includes ----*/
#include "graphics/sprite2D.hpp"

#include "toolbox/debug.h"


/*---- Global Variables ----*/
/*** init static members ***/
TextureID_t Sprite2D::last_texture = 0;
//glTextureUnit Sprite2D::last_glTexture = 0;
VertexArrayObj_t Sprite2D::last_vao = 0;
VertexBuffObj_t Sprite2D::last_txVBO = 0;
glm::vec4 Sprite2D::v4_last_tint = glm::vec4 (0.f);


/*---- Methods Implementation ----*/
/**
 * @brief Base Constructor, just a white square with no size.
 */
Sprite2D::Sprite2D ()
{
    const static GLfloat vertices[] = {0.f, 1.f,  // top left
                                       1.f, 0.f,  // bottom right
                                       0.f, 0.f,  // bottom left
                                       1.f, 1.f}; // top right

    const static GLuint indices[] = {0, 1, 2,  // first triangle
                                     0, 3, 1}; // second triangle

    static SharedGlObjs gl_square_area(sizeof(vertices), vertices, sizeof(indices), indices);

    _debug (TRACE, "::%s ()", __FUNCTION__);

    //setup triangles
    glGenVertexArrays (1, &this->VAO);
    glBindVertexArray (this->VAO);
    Sprite2D::last_vao = this->VAO;

    glBindBuffer (GL_ARRAY_BUFFER, gl_square_area.getVBO ());
    glEnableVertexAttribArray (0);
    glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);

    glGenBuffers (1, &this->txVBO);
    glBindBuffer (GL_ARRAY_BUFFER, this->txVBO);
    Sprite2D::last_txVBO = this->txVBO;
    glBufferData (GL_ARRAY_BUFFER, sizeof(this->f_texture_uv), this->f_texture_uv, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray (1);
    glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);

    glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, gl_square_area.getEBO ());
}//End Constructor


/**
 * @brief Destructor
 */
Sprite2D::~Sprite2D ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    //clean the OpenGl buffers
    if (Sprite2D::last_vao == this->VAO)
        Sprite2D::last_vao = 0;
    glDeleteVertexArrays (1, &this->VAO);

    if (Sprite2D::last_txVBO == this->txVBO)
        Sprite2D::last_txVBO = 0;
    glDeleteBuffers (1, &this->txVBO);

    //delete the texture map if we were using one
    if (this->v_texture_map)
        delete this->v_texture_map;
}//End Destructor

/**
 * @brief Sets a Texture to be used by the Sprite.
 *
 * The size of the sprite will be set to the original image size in pixels.<br>
 * If the Texture has a tile map, one will be created for the Sprite as well, but empty (not all tiles need to be used by
 *    a single sprite).<br>
 * Overriding the current Texture is permitted, even a null texture if you want it to go back to a colored square.
 *
 * @param t - pointer to the texture to be used by the sprite.
 *
 * @remark Remember to create a tile map for the Texture <i>before</i> calling this method, or the Sprite will have no
 *     map of its own.
 */
void Sprite2D::setTexture (Texture *t)
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

#ifdef ENABLE_DEBUG
    if (!t)
        _debug (WARNING, "Trying to set texture to null, are your sure?");
#endif

    if (this->p_texture)
    {
        _debug (WARNING, "This texture was already set, are you sure it should be overridden?");

        if (this->v_texture_map)
        {
            _debug (WARNING, "This sprite also had a tile map, all its data will be lost...");

            delete this->v_texture_map;
            this->v_texture_map = nullptr;
        }//end if (this->gl_map)
    }//end if (this->texture != nullptr)

    this->p_texture = t;

    if (t)
    {
        //considering our projection matrix is the size of the viewport, this should work;
        this->getSize().set (t->getWidth (), t->getHeight ());

        //if texture has a tile map, create one for the sprite too (but empty).
        if (t->getTileMap ())
            this->v_texture_map = new std::vector<unsigned> ();
    }//end if (t)
}//End setTexture ()


/**
 * @brief Mirror the texture around an axis.
 *
 * @param arround_x - true if should mirror around the x axle.
 * @param arround_y - true if should mirror around the y axle.
 */
void Sprite2D::mirror (bool arround_x, bool arround_y)
{
    glm::vec2 aux = glm::vec2 ((arround_x ? -1.f : 1.f), (arround_y ? -1.f : 1.f));

    _debug (TRACE, "::%s (x=%s, y=%s)", __FUNCTION__, (arround_x ? "true" : "false"), (arround_y ? "true" : "false"));

    if (this->v2_flip == aux)
        return; //avoid rebuilding model

    this->v2_flip = aux;
    this->b_model_dirty_flag = true;
}//end mirror ()

//Just a helper method to rebuild the Model Matrix in case any parameter changed between draw calls.
//(transformations are: scale happens first, then rotation and then final translation happens; reversed order)
void Sprite2D::_rebuildModel ()
{
    //save local references to complex structures accessed more than once
    const glm::vec2 &rotation_point = this->f_angle.getRotationPoint ();
    const glm::vec3 &current_size = this->v3_size.get (),
                    scaled_size = this->v3_size.getScaled ();

    /* sprite atributes have changed, rebuild the model */
    this->m4_model_cash = glm::mat4 (1.0f);

    //correct mirror displacement if needed
    if (this->v2_flip.x == -1)
        this->m4_model_cash = glm::translate(this->m4_model_cash , glm::vec3(scaled_size.x, 0.f, 0.f));
    if (this->v2_flip.y == -1)
        this->m4_model_cash = glm::translate(this->m4_model_cash , glm::vec3(0.f, scaled_size.y, 0.f));

    //move to world position
    this->m4_model_cash = glm::translate (this->m4_model_cash, this->v3_position.get ());

    //then flip the texture if needed
    this->m4_model_cash = glm::scale (this->m4_model_cash, glm::vec3 (this->v2_flip, 1.f));

    //move origin of rotation to center of quad
    this->m4_model_cash = glm::translate (this->m4_model_cash, glm::vec3(rotation_point.x * current_size.x,
                                                                         rotation_point.y * current_size.y,
                                                                         0.f));
    this->m4_model_cash = glm::rotate (this->m4_model_cash, glm::radians(this->f_angle.get ()), glm::vec3(0.f, 0.f, 1.f)); //then rotate
    //move origin back
    this->m4_model_cash = glm::translate (this->m4_model_cash, glm::vec3(-rotation_point.x * current_size.x,
                                                                         -rotation_point.y * current_size.y,
                                                                         0.f));

    //render it the correct size
    this->m4_model_cash = glm::scale (this->m4_model_cash, scaled_size);

    this->b_model_dirty_flag = false;
    this->v3_position.resetFlag ();
    this->f_angle.resetFlag ();
    this->v3_size.resetFlag ();
}//End rebuildModel ()

/**
 * @brief Renders the sprite on the screen.
 *
 * @param shader - reference to the Shader to be used to render.
 */
void Sprite2D::draw (Shader &shader)
{
    //sync the entangled properties
    this->v3_position.update ();
    this->f_angle.update ();
    this->v3_size.update ();
    this->v4_color_tint.update ();

    // prepare transformations
    if (this->b_model_dirty_flag || this->v3_position.isDirty () || this->f_angle.isDirty () || this->v3_size.isDirty ())
        this->_rebuildModel ();
    shader.setUniform ("model", this->m4_model_cash);

    //we can ignore the color dirty flag.
    if (this->v4_last_tint != this->v4_color_tint.get ())
    {
        shader.setUniform ("color", this->v4_color_tint.get ());
        this->v4_last_tint = this->v4_color_tint.get ();
    }//end if (this->v4_last_tint != this->v4_color_tint.get ())

    /*
     *if (this->glTexture != Sprite2D::last_glTexture)
     *{
     *    glActiveTexture (this->glTexture);
     *    this->last_glTexture = this->glTexture;
     *}//end if (glTexture != this->last_glTexture)
     */
    if (this->p_texture && this->p_texture->getID() != Sprite2D::last_texture)
    {
        this->p_texture->bind ();
        Sprite2D::last_texture = this->p_texture->getID ();
    }//end if (this->p_texture && ...
    else if (!this->p_texture && Sprite2D::last_texture != 0)
    {
        glBindTexture (GL_TEXTURE_2D, 0);
        Sprite2D::last_texture = 0;
    }//end else if (!this->p_texture && ...

    if (this->VAO != Sprite2D::last_vao)
    {
        glBindVertexArray (this->VAO);
        Sprite2D::last_vao = this->VAO;
    }//end if (this->VAO != this->last_vao)

    //Draw to the screen buffer
    glDrawElements (GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}//End draw ()


/**
 * @brief Set the sprite to render just a section of the texture.
 *
 * OpenGl has no idea what pixel coordinates are, it makes no sense, so it uses a "percentage"
 *    value to know how much of the texture we want to show.
 *
 * @remark This is the method that actually sets the render to crop the texture.
 *
 * @param x1 - left coordinate of the section.
 * @param y1 - top coordinate of the section.
 * @param x2 - right coordinate of the section.
 * @param y2 - bottom coordinate of the section.
 */
void Sprite2D::crop (float x1, float y1, float x2, float y2)
{
    _debug (TRACE, "::%s ([%.4f,%.4f,%.4f,%.4f])", __FUNCTION__, x1, y1, x2, y2);

    //top left
    this->f_texture_uv[0] = x1;
    this->f_texture_uv[1] = y2;

    //bottom right
    this->f_texture_uv[2] = x2;
    this->f_texture_uv[3] = y1;

    //bottom left
    this->f_texture_uv[4] = x1;
    this->f_texture_uv[5] = y1;

    //top right
    this->f_texture_uv[6] = x2;
    this->f_texture_uv[7] = y2;

    //Set UV coordinates for the texture
    if (this->txVBO != Sprite2D::last_txVBO)
    {
        Sprite2D::last_txVBO = this->txVBO;
        glBindBuffer (GL_ARRAY_BUFFER, this->txVBO);
    }//end if (this->txVBO != Sprite2D::last_txVBO)
    glBufferSubData (GL_ARRAY_BUFFER, 0, sizeof(this->f_texture_uv), this->f_texture_uv);
}//End crop ()

/**
 * @brief Set the sprite to render just a section of the texture.
 *
 * A more reasonable method that uses pixel coordinates to mark the clipping area.<br>
 *
 * @remark The equivalent OpenGl's coordinates will be calculated on the fly on every call, if this is a common
 *            occurrence, you may want to pre-map your textures for better performance (in exchange for a little memory).
 *
 * @param x1 - left pixel coordinate of the section.
 * @param y1 - top pixel coordinate of the section.
 * @param x2 - right pixel coordinate of the section.
 * @param y2 - bottom pixel coordinate of the section.
 * @param resize - <b>true</b> if the sprite should be resized to the new texture section size.
 */
void Sprite2D::crop (unsigned x1, unsigned y1, unsigned x2, unsigned y2, bool resize)
{
    unsigned tx_w = 0,
             tx_h = 0;

    _debug (TRACE, "::%s (x1=%d, y1=%d, x2=%d, y2=%d, resize=%s)", __FUNCTION__, x1, y1, x2, y2, (resize ? "true" : "false"));

    if (!this->p_texture)
        throw std::runtime_error (fmt::format ("{0}:{1} - Sprite has no Texture!", __LINE__, __FILE__));

    tx_w = this->p_texture->getWidth ();
    tx_h = this->p_texture->getHeight ();

    this->crop (pixel2glmCoordinate (tx_w, x1),
                         pixel2glmCoordinate (tx_h, y1),
                         pixel2glmCoordinate (tx_w, x2),
                         pixel2glmCoordinate (tx_h, y2));

    if (resize)
        this->v3_size.set (x2-x1, y2-y1);
}//End crop ()

/**
 * @brief Set the sprite to render just a section of the texture using the Texture's map of tiles.
 *
 * A much more reasonable method that uses pixel coordinates from a Texture's tile map to mark the clipping area.<br>
 *
 * @param tile_index - index of the Texture's tile to be used.
 * @param resize - <b>true</b> if the sprite should be resized to the new texture section size.
 *
 * @throws std::runtime_error - if the sprite has no texture or its texture has no map.
 * @throws std::out_of_range - if the tile index is greater than the max number of tiles supported.
 */
void Sprite2D::cropUsingTxTileMap (unsigned tile_index, bool resize)
{
    const Texture::tile_uv *tile = nullptr;

    _debug (TRACE, "::%s (tile=%u, resize=%s)", __FUNCTION__, tile_index, (resize ? "true" : "false"));

    if (!this->p_texture)
        throw std::runtime_error (fmt::format ("{0}:{1} - Sprite has no Texture!", __LINE__, __FILE__));

    tile = &this->p_texture->getTileLimits (tile_index);
    this->crop (tile->f_x1, tile->f_y1,
                         tile->f_x2, tile->f_y2);

    if (resize)
        this->v3_size.set (tile->u_width, tile->u_height);
}//End setTxClipAreaFromTxMap ()

/**
 * @brief Set the sprite to use all the tiles from the Texture.
 * @remark Unless you need to animate the Sprite, this method is rather redundant.
 *
 * @throws std::runtime_error - if the sprite has no texture or its texture has no map.
 */
void Sprite2D::addAllTxTilesToList ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    this->addContiguousTxTilesToList (0, this->p_texture->getTilesCount() - 1);
}//End createTileList ()

/**
 * @brief Adds a contiguous selection of tiles from the Texture's tile map.
 *
 * @param tx_start - index of the first tile.
 * @param tx_end - index of the last tile.
 *
 * @throws std::runtime_error - if the sprite has no texture or its texture has no map.
 * @throws std::out_of_range - if the tile index is greater than the max number of tiles supported.
 */
void Sprite2D::addContiguousTxTilesToList (unsigned tx_start, unsigned tx_end)
{
    unsigned tile_count = 0;

    _debug (TRACE, "::%s (tx_start=%u, tx_end=%u)", __FUNCTION__, tx_start, tx_end);

    if (!this->p_texture)
        throw std::runtime_error (fmt::format ("{0}:{1} - Sprite has no Texture!", __LINE__, __FILE__));

    tile_count = this->p_texture->getTilesCount ();
    if (!tile_count)
        throw std::runtime_error (fmt::format ("{0}:{1} - Texture has no tile map!", __LINE__, __FILE__));
    if (tx_end >= tile_count)
        throw std::out_of_range (fmt::format ("{0}:{1} - Tile out of range, {2} of max {3}!", __LINE__, __FILE__, tx_end, tile_count));

    //It would be better to call addTileToList() -N times, but this is a game engine, gotta go fast!!!
    this->v_texture_map->reserve (this->v_texture_map->size() + (tx_end - tx_start) +  1);
    for (; tx_start <= tx_end; ++tx_start)
        this->v_texture_map->push_back (tx_start);

    //make sure the sprite won't be the size of the image atlas!
    this->cropUsingSpTileMap (0, true);
}//End addContiguousTxTilesToList ()

/**
 * @brief Adds a single tile from the Texture's tile map to the sprite list of tiles.
 *
 * @param tile_index - index of the Texture tile to be mapped.
 *
 * @throws std::runtime_error - if the sprite has no texture or its texture has no map.
 * @throws std::out_of_range - if the tile index is greater than the max number of tiles supported.
 */
void Sprite2D::addTxTileToList (unsigned tile_index)
{
    unsigned tile_count = 0;

    _debug (TRACE, "::%s (tx_tile=%u)", __FUNCTION__, tile_index);

    if (!this->p_texture)
        throw std::runtime_error (fmt::format ("{0}:{1} - Sprite has no Texture!", __LINE__, __FILE__));

    tile_count = this->p_texture->getTilesCount ();
    if (!tile_count)
        throw std::runtime_error (fmt::format ("{0}:{1} - Texture has no tile map!", __LINE__, __FILE__));
    if (tile_index >= tile_count)
        throw std::out_of_range (fmt::format ("{0}:{1} - Tile out of range, {2} of max {3}!", __LINE__, __FILE__, tile_index, tile_count));

    this->v_texture_map->push_back (tile_index);

    //make sure the sprite won't be the size of the image atlas!
    if (this->v_texture_map->size() == 1)
        this->cropUsingSpTileMap (0, true);
}//End addTileToList ()

/**
 * @brief Set the sprite to render just a section of the texture using its own list of mapped tiles.
 *
 * @param s_tile_index - the index of the tile to be rendered.
 * @param resize - <b>true</b> if the sprite should be resized to the new texture section size.
 *
 * @throws std::runtime_error - if the sprite has no texture or its texture has no map.
 * @throws std::out_of_range - if the tile index is greater than the max number of tiles supported.
 */
void Sprite2D::cropUsingSpTileMap (unsigned s_tile_index, bool resize)
{
    const Texture::tile_uv *tile = nullptr;

    _debug (TRACE, "::%s (sprite_tile=%u, resize=%s)", __FUNCTION__, s_tile_index, (resize ? "true" : "false"));

    if (!this->v_texture_map)
        throw std::runtime_error (fmt::format ("{0}:{1} - Sprite has no map allocated!", __LINE__, __FILE__));
    else if (s_tile_index >= this->v_texture_map->size ())
        throw std::out_of_range (fmt::format ("{0}:{1} - Sprite has no tile {2}, max {3}!", __LINE__, __FILE__, s_tile_index, this->v_texture_map->size ()));

    tile = &this->p_texture->getTileLimits (this->v_texture_map->at (s_tile_index));
    this->crop (tile->f_x1, tile->f_y1, tile->f_x2, tile->f_y2);

    if (resize)
        this->v3_size.set (tile->u_width, tile->u_height);
}//End cropUsingSpTileMap ()


/**
 * @brief Sets the animation Frames Per Second to be used by update()
 *
 * @param fps - the animation fps.
 * @param loop - if the animation should loop or cycle once and stop.
 */
void Sprite2D::setAnimationFPS (float fps, bool loop)
{
    _debug (TRACE, "::%s (fps=%.2f, loop=%s)", __FUNCTION__, fps, (loop ? "true" : "false"));

    this->f_animation_refresh_t = 1.f/fps;
    this->b_animation_loop = loop;
}//End setAnimationFPS ()

/**
 * @brief Sets the animation sequence back to frame 0.
 */
void Sprite2D::resetAnimation (bool reset_delta_t)
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (this->f_animation_refresh_t == 0.f)
    {
        _debug (WARNING, "Not a animated sprite, ignoring....");
        return;
    }//end if (this->f_animation_refresh_t == 0.f)

    if (reset_delta_t)
        this->f_animation_delta_t = 0.f;
    this->u_current_frame = 0;
    this->cropUsingSpTileMap (0, true);
}//End resetAnimation ()

/**
 * @brief Updates the animation sequence based on the set FPS and current time elapsed.
 *
 * @remark Does nothing if the animation FPS is zero.
 *
 * @param delta_t - time elapsed since the last game loop start.
 *
 * @return <b>true</b> if animation is being updated; <b>false</b> if animation cycle finished and require reset.
 */
bool Sprite2D::update (double delta_t)
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (this->f_animation_refresh_t == 0.f)
    {
        _debug (WARNING, "Not a animated sprite, ignoring....");
        return false;
    }//end if (this->f_animation_refresh_t == 0.f)

    if (!this->b_animation_loop &&
            this->u_current_frame >= this->v_texture_map->size ())
        return false; //animation cycled once, wait for reset

    this->f_animation_delta_t += delta_t;
    if (this->f_animation_delta_t >= this->f_animation_refresh_t)
    {
        ++this->u_current_frame;

        if (this->u_current_frame < this->v_texture_map->size ())
            this->cropUsingSpTileMap (this->u_current_frame, true);
        else if (this->b_animation_loop)
            this->resetAnimation (false);

        //get the remaining from last update
        this->f_animation_delta_t -= this->f_animation_refresh_t;
    }//End if (f_animation_delta_t >= this->f_animation_refresh_t)

    return true;
}//End update ()

